feat: deletion APIs + secure_delete for disappearing messages (3/3)#315
feat: deletion APIs + secure_delete for disappearing messages (3/3)#315dannym-arx wants to merge 2 commits into
Conversation
Adds local deletion primitives so clients can enforce NIP-40 expiration on their own schedule, and hardens SQLite against forensic recovery of deleted content. New MessageStorage trait methods: - delete_message(group_id, event_id) -> bool - delete_messages_before_timestamp(group_id, before) -> usize - delete_processed_messages_for_group(group_id) -> usize Implementations in mdk-memory-storage and mdk-sqlite-storage. The memory backend scopes messages_cache eviction to the owning group to avoid evicting another group's entry under coincident EventIds. mdk-core exposes matching public methods on MDK. UniFFI surfaces all three over the FFI boundary. SQLite connection init now sets PRAGMA secure_delete = ON so deleted rows (including expired disappearing messages) are overwritten with zeros on disk. Final part of the 3-PR split: - Part 1: #258 (MLS extension v3 wire format) - Part 2: #306 (validation + NIP-40 auto-expiration + tri-state)
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
✅ Files skipped from review due to trivial changes (2)
📝 WalkthroughWalkthroughThis PR adds three deletion APIs—delete_message (single event), delete_messages_before_timestamp (bulk by creation time), and delete_processed_messages_for_group (processed-message metadata)—across the MessageStorage trait, MDK core, memory and SQLite storage implementations, and UniFFI bindings; SQLite now enables PRAGMA secure_delete on init. ChangesGranular Message Deletion Feature
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 6✅ Passed checks (6 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
Ready to review this PR? Stage has broken it down into 5 individual chapters for you: Chapters generated by Stage for commit 9e79651 on May 22, 2026 11:52am UTC. |
❌ Coverage: 94.51% → 94.44% (-0.07%) |
…PR refs The cherry-pick from the original combined PR #253 placed the deletion-API and secure_delete entries in the [0.8.0] section because that PR predated the 0.8.0 release cut. Move them to Unreleased and append the #315 PR link, matching the convention used for Part 1 (#258) and Part 2 (#306). Also adds the missing mdk-uniffi changelog entry for the new deletion bindings.
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (3)
crates/mdk-memory-storage/src/messages.rs (1)
413-414: 💤 Low valueConsider importing
HashSetat the top for consistency.
HashMapis imported at the file top (line 3) whileHashSetis used with a full path here. Minor style inconsistency.Suggested fix
At the top of the file:
-use std::collections::HashMap; +use std::collections::{HashMap, HashSet};Then here:
-let mut all_ids: std::collections::HashSet<EventId> = to_remove.iter().copied().collect(); +let mut all_ids: HashSet<EventId> = to_remove.iter().copied().collect();🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/mdk-memory-storage/src/messages.rs` around lines 413 - 414, Import HashSet alongside HashMap at the top of the file and replace the fully-qualified type usage in the all_ids declaration; specifically, add HashSet to the existing imports and change the line creating all_ids (currently using std::collections::HashSet<EventId>) to use HashSet<EventId>, keeping the variables to_remove, orphaned and type EventId unchanged.crates/mdk-sqlite-storage/src/messages.rs (2)
390-399: ⚡ Quick winAdd an index for
processed_messages.mls_group_id.This new per-group cleanup path will full-scan
processed_messageswith the current schema shape. The visible index coverage only includesmessage_event_id,state, andprocessed_at, so disappearing-message cleanup cost grows with the entire dedup table instead of the target group.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/mdk-sqlite-storage/src/messages.rs` around lines 390 - 399, The delete_processed_messages_for_group cleanup will full-scan processed_messages because there is no index on processed_messages.mls_group_id; add a database index for that column (e.g., CREATE INDEX IF NOT EXISTS on mls_group_id) as part of your schema/migration so the delete in delete_processed_messages_for_group can be satisfied by the index and avoid scanning the entire dedup table; ensure the migration/initialization path that creates the processed_messages table also creates the idx_processed_messages_mls_group_id index (or add a new migration step) so existing deployments get the index applied.
1246-1253: ⚡ Quick winCover the file-backed
secure_deletepath too.This assertion only exercises
new_in_memory().PRAGMA secure_deleteis also set inopen_connection(), and that persistent path is the security-sensitive one for on-disk recovery.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/mdk-sqlite-storage/src/messages.rs` around lines 1246 - 1253, Test only covers the in-memory path; add a second test that constructs a file-backed MdkSqliteStorage so open_connection() is exercised and then query "PRAGMA secure_delete" via with_connection(...) and assert it equals 1; specifically, keep the existing secure_delete_pragma_is_enabled test using MdkSqliteStorage::new_in_memory() and add a new test that creates a persistent/file-backed storage (so open_connection() runs), runs conn.query_row("PRAGMA secure_delete", [], |row| row.get(0)) inside with_connection, and asserts the returned i64 == 1.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/mdk-core/CHANGELOG.md`:
- Line 94: Update the changelog bullet for the Unreleased / Added entry that
mentions the public methods delete_message, delete_messages_before_timestamp,
and delete_processed_messages_for_group by appending the required PR reference
in the mandated format:
"([`#123`](https://github.com/marmot-protocol/mdk/pull/123))" (replace 123 with
the actual PR number that introduced these methods) so the line ends with the
trailing PR link.
In `@crates/mdk-memory-storage/CHANGELOG.md`:
- Line 59: Move the changelog line "Implemented `delete_message`,
`delete_messages_before_timestamp`, and `delete_processed_messages_for_group`
for per-message and bulk expiry-based deletion." out of the released `##
[0.8.0]` section and place it under `## Unreleased` within the `### Added`
subsection; then append the PR reference in the required format, e.g.
`([`#123`](https://github.com/marmot-protocol/mdk/pull/123))`, to the end of that
entry so it follows the project's changelog guidelines.
In `@crates/mdk-sqlite-storage/CHANGELOG.md`:
- Around line 60-61: Move the two bullets currently listed under the released
"0.8.0" section into the "## Unreleased" section of CHANGELOG.md and append the
PR reference suffix to each entry as
"([`#315`](https://github.com/your-repo/pull/315))"; specifically update the
bullets "Implemented `delete_message`, `delete_messages_before_timestamp`, and
`delete_processed_messages_for_group`..." and "Enabled `PRAGMA secure_delete =
ON`..." so they live under "## Unreleased" and end with the required PR link.
In `@crates/mdk-storage-traits/CHANGELOG.md`:
- Line 53: The two new changelog bullets that mention
MessageStorage::delete_message,
MessageStorage::delete_messages_before_timestamp, and
MessageStorage::delete_processed_messages_for_group are incorrectly placed under
the historical "0.8.0" section and lack the PR link; move these entries into the
"## Unreleased" section and append the PR reference
"([`#315`](https://github.com/marmot-protocol/mdk/pull/315))" to each bullet so
they follow the project's changelog convention.
In `@crates/mdk-uniffi/src/lib.rs`:
- Around line 1291-1292: The conversion of the deletion result uses a lossy cast
(`as u32`) which can truncate large `usize` counts; replace the cast with a
checked conversion (e.g. `u32::try_from(count)` or equivalent) and map the Err
branch to an explicit error return so overflow is reported instead of silently
truncated—apply this change to the `let count =
mdk.delete_messages_before_timestamp(&group_id, before)?; Ok(count as u32)` site
and the analogous `count` -> `u32` conversion at the other occurrence around
lines 1304–1305, returning a clear error (with context like "deletion count
overflow") when the conversion fails.
---
Nitpick comments:
In `@crates/mdk-memory-storage/src/messages.rs`:
- Around line 413-414: Import HashSet alongside HashMap at the top of the file
and replace the fully-qualified type usage in the all_ids declaration;
specifically, add HashSet to the existing imports and change the line creating
all_ids (currently using std::collections::HashSet<EventId>) to use
HashSet<EventId>, keeping the variables to_remove, orphaned and type EventId
unchanged.
In `@crates/mdk-sqlite-storage/src/messages.rs`:
- Around line 390-399: The delete_processed_messages_for_group cleanup will
full-scan processed_messages because there is no index on
processed_messages.mls_group_id; add a database index for that column (e.g.,
CREATE INDEX IF NOT EXISTS on mls_group_id) as part of your schema/migration so
the delete in delete_processed_messages_for_group can be satisfied by the index
and avoid scanning the entire dedup table; ensure the migration/initialization
path that creates the processed_messages table also creates the
idx_processed_messages_mls_group_id index (or add a new migration step) so
existing deployments get the index applied.
- Around line 1246-1253: Test only covers the in-memory path; add a second test
that constructs a file-backed MdkSqliteStorage so open_connection() is exercised
and then query "PRAGMA secure_delete" via with_connection(...) and assert it
equals 1; specifically, keep the existing secure_delete_pragma_is_enabled test
using MdkSqliteStorage::new_in_memory() and add a new test that creates a
persistent/file-backed storage (so open_connection() runs), runs
conn.query_row("PRAGMA secure_delete", [], |row| row.get(0)) inside
with_connection, and asserts the returned i64 == 1.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: cada1cf7-0394-4f92-9bd1-e9a4f492c7c7
📒 Files selected for processing (10)
crates/mdk-core/CHANGELOG.mdcrates/mdk-core/src/groups.rscrates/mdk-memory-storage/CHANGELOG.mdcrates/mdk-memory-storage/src/messages.rscrates/mdk-sqlite-storage/CHANGELOG.mdcrates/mdk-sqlite-storage/src/lib.rscrates/mdk-sqlite-storage/src/messages.rscrates/mdk-storage-traits/CHANGELOG.mdcrates/mdk-storage-traits/src/messages/mod.rscrates/mdk-uniffi/src/lib.rs
Final piece of the 3-PR split for disappearing messages. The first PR wired the field through the MLS extension, the second added validation and the NIP-40 auto-expiration tag, and this one hands clients the deletion plumbing plus a SQLite hardening that keeps deleted rows from lingering on disk.
This is the part where marmots learn to clean up after themselves.
What's in it
New MessageStorage trait methods:
delete_message(group_id, event_id) -> bool: single-message deletedelete_messages_before_timestamp(group_id, before) -> usize: bulk delete by age (caller computesnow - duration)delete_processed_messages_for_group(group_id) -> usize: clears the dedup metadata for a groupImplementations across mdk-memory-storage and mdk-sqlite-storage. The memory backend scopes
messages_cacheeviction to the owning group, so a coincident EventId in another group can't get yanked out by accident.MDK public API:
Matching wrapper methods on
MDKso clients can call these without going through the storage trait directly.UniFFI:
All three deletion methods surfaced over the FFI boundary for Kotlin and Swift consumers.
SQLite hardening:
PRAGMA secure_delete = ONon every connection init. When a row is deleted, SQLite overwrites the freed bytes with zeros instead of just unlinking them. An expired disappearing message no longer sits recoverable in the database file for a forensic tool to lift later.Part of the split
Testing
Tests added for each new trait method on both storage backends. PRAGMA secure_delete verification test.
just precommitgreen across all features.What this doesn't do
MDK doesn't run a background sweeper. Clients call
delete_messages_before_timestampon whatever cadence makes sense for them. The primitives ship here; clients pick the policy.This PR completes the disappearing messages feature by adding per-message and bulk deletion APIs across storage layers and exposing them through MDK and UniFFI, and it hardens SQLite by enabling PRAGMA secure_delete to reduce forensic recoverability of deleted rows. Clients are expected to call the new bulk-deletion API on their chosen cadence because MDK does not run a background sweeper.
What changed:
Security impact:
API surface:
Testing: